﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GameLibrary.Map
{
    using Settlement;
    using Unit;
    using Events;

    /// <summary>
    /// Handle add,del,get,contains action in player map 
    /// Also return costmap(double[,]) and balanced adjancency list
    /// </summary>
    [Serializable]
    public class RegionMap
    {
        #region variables

        /// <summary>
        /// Adjancency list style map
        /// </summary>
        private AdjancencyList<RegionInfo,RegionInfo> map;

        /// <summary>
        /// Visited regions
        /// </summary>
        private Dictionary<Position, RegionInfo> regionDictionary;

        #endregion

        #region constructor

        internal RegionMap()
        {
            this.map = new AdjancencyList<RegionInfo,RegionInfo>();
            this.regionDictionary = new Dictionary<Position, RegionInfo>();
        }

        #endregion

        #region this

        internal RegionInfo this[Position pos]
        {
            get
            {
                if (this.regionDictionary.ContainsKey(pos) == true)
                    return this.regionDictionary[pos];
                else
                    return null;
            }
        }

        #endregion

        #region Add

        /// <summary>
        /// Add settlement into gamemap and add its info into regioninfo
        /// </summary>
        /// <param name="settlement"></param>
        internal void AddSettlement(Settlement settlement)
        {
            GameMap.UniqueInstance.AddSettlement(settlement);
            Position key = MapHelper.FindRegion(settlement.Location);
            if (regionDictionary.ContainsKey(key) == false)
                this.regionDictionary.Add(key, new RegionInfo(key));
            this.regionDictionary[key].AddSettlement(settlement.Info);
        }

        /// <summary>
        /// Add unit into gamemap and its info into regioninfo
        /// </summary>
        /// <param name="unit"></param>
        internal void AddUnit(Unit unit)
        {
            GameMap.UniqueInstance.AddUnit(unit);
            Position key = MapHelper.FindRegion(unit.Location);
            if (regionDictionary.ContainsKey(key) == false)
                this.regionDictionary.Add(key, new RegionInfo(key));
            this.regionDictionary[key].AddUnit(unit.Info);
        }

        #endregion

        #region Remove

        internal void RemoveSettlement(uint settlementID)
        {
            Position location = GameMap.UniqueInstance.GetSettlementByID(settlementID).Location;
            this.regionDictionary[MapHelper.FindRegion(location)].RemoveSettlement(settlementID);
            GameMap.UniqueInstance.RemoveSettlement(settlementID);            
        }

        internal void RemoveUnit(uint unitID)
        {
            Position location = GameMap.UniqueInstance.GetUnitByID(unitID).Location;
            this.regionDictionary[MapHelper.FindRegion(location)].RemoveUnit(unitID);
            GameMap.UniqueInstance.RemoveUnit(unitID);
        }

        #endregion

        #region Refresh

        /// <summary>
        /// Reset all the information in this region
        /// </summary>
        /// <param name="regionPosition"></param>
        internal void RefreshRegion(Position regionPosition)
        {
            if (this.regionDictionary.ContainsKey(regionPosition) == true) // already visited once
                this.regionDictionary[regionPosition].Refresh();
            else // new reached region
            {
                RegionInfo info = new RegionInfo(regionPosition);
                this.regionDictionary.Add(regionPosition, info);
                this.map.AddKey(info);
                foreach (var pos in MapHelper.SurrondingPositions) // can be seen as BFS on this position
                {
                    Position tmp = pos + regionPosition;
                    if (this.regionDictionary.ContainsKey(tmp) == true) // surronding position already visited
                    {
                        // add to both direction
                        this.map.AddValue(this.regionDictionary[regionPosition], this.regionDictionary[tmp]);
                        this.map.AddValue(this.regionDictionary[tmp], this.regionDictionary[regionPosition]);
                    }
                }
            }
        }

        /// <summary>
        /// Reset the info of a settlement
        /// </summary>
        /// <param name="settlement"></param>
        internal void RefreshSettlement(Settlement settlement)
        {
            Position region = MapHelper.FindRegion(settlement.Location);
            this.regionDictionary[region].RemoveSettlement(settlement.ID);
            this.regionDictionary[region].AddSettlement(settlement.Info);
        }

        /// <summary>
        /// Reset info of a unit
        /// </summary>
        /// <param name="unit"></param>
        /// <param name="oldPosition"></param>
        internal void RefreshUnit(Unit unit,Position oldPosition)
        {
            Position region = MapHelper.FindRegion(oldPosition);
            this.regionDictionary[region].RemoveUnit(unit.ID);
            region = MapHelper.FindRegion(unit.Location);
            this.regionDictionary[region].AddUnit(unit.Info);
        }

        #endregion

        #region Get

        internal SettlementInfo GetSettlementInfoByPosition(Position pos)
        {
            Position regPos = MapHelper.FindRegion(pos);
            var reg = this.GetRegionInfo(regPos);
            return reg.GetSettlementByPosition(pos);
        }

        internal IEnumerable<UnitInfo> GetUnitInfosByPosition(Position pos)
        {
            var regPos = MapHelper.FindRegion(pos);
            var reg = this.GetRegionInfo(regPos);
            return reg.GetUnitsByPosition(pos);
        }

        internal RegionInfo GetRegionInfo(Position regionPosition)
        {
            return this.regionDictionary[regionPosition];
        }

        /// <summary>
        /// Get all the region info with position in regions.
        /// </summary>
        /// <param name="regions"></param>
        /// <returns></returns>
        internal List<RegionInfo> GetRegionInfos(List<Position> regions)
        {
            List<RegionInfo> list = new List<RegionInfo>();
            foreach (Position pos in regions)
                if (this.regionDictionary.ContainsKey(pos) == true)
                    list.Add(new RegionInfo(this.regionDictionary[pos]));
            return list;
        }

        /// <summary>
        /// Return all region info which known by the player
        /// </summary>
        /// <returns></returns>
        internal AdjancencyList<RegionInfo,RegionInfo> GetKnownMap()
        {
            return new AdjancencyList<RegionInfo,RegionInfo>(this.map);
        }

        /// <summary>
        /// Return the neighbor region of a region with position pos
        /// </summary>
        /// <param name="pos"></param>
        /// <returns></returns>
        internal List<RegionInfo> GetAdjancencyRegion(Position pos)
        {
            var region = this.regionDictionary[pos];
            return new List<RegionInfo>(this.map.Values(region));
        }

        /// <summary>
        ///  Get all the settlements in a region with region position
        /// </summary>
        /// <param name="regionPosition"></param>
        /// <returns></returns>
        internal IEnumerable<SettlementInfo> GetSettlementInfosInARegion(Position regionPosition)
        {
            if (this.regionDictionary.ContainsKey(regionPosition) == true)
            {
                return this.regionDictionary[regionPosition].GetSettlements();
            }
            else
                return null;
        }

        public IEnumerable<RegionInfo> GetAllRegions()
        {
            return this.regionDictionary.Values;
        }

        #endregion

   

        #region border part 

        public AdjancencyList<RegionInfo,RegionInfo> GetBorderRegions(uint playerID)
        {
            var dic = this.BreadthFirstSearch(playerID);
            AdjancencyList<RegionInfo,RegionInfo> list = 
                new AdjancencyList<RegionInfo,RegionInfo>();
            foreach (var pair in dic)
            {
                list.AddKey(pair.Key);
                foreach (var reg in pair.Value)
                    list.AddValue(pair.Key, reg);
            }
            return list;
        }

        #region BFS

        private Dictionary<RegionInfo,HashSet<RegionInfo>> BreadthFirstSearch(uint playerID)
        {
            HashSet<RegionInfo> regions = new HashSet<RegionInfo>(this.map.Keys);
            HashSet<RegionInfo> regionsWhichAreNotBelongToPlayer = 
                new HashSet<RegionInfo>();
            Dictionary<RegionInfo, HashSet<RegionInfo>> balancedList =
                new Dictionary<RegionInfo, HashSet<RegionInfo>>();

            Queue<RegionInfo> queue = new Queue<RegionInfo>();

            queue.Enqueue(regions.First());
            regions.Remove(queue.First());

            while (regions.Count > 0) // there are regions which have not been searched.
            {
                if (queue.Count <= 0) // handle unconnected parts
                {
                    queue.Enqueue(regions.First());
                }
                while (queue.Count > 0) // BFS
                {
                    RegionInfo reg = queue.Dequeue();

                    foreach (RegionInfo r in this.map.Values(reg))
                    {
                        if (balancedList.ContainsKey(r) == false) 
                        {
                            balancedList.Add(r, new HashSet<RegionInfo>()); // add new visited region
                            if (this.regionDictionary[r.Start].GetSettlementByOwner(playerID) != null)
                                 // only enqueue region with player's city
                            {
                                queue.Enqueue(r);
                            }
                            else
                                regionsWhichAreNotBelongToPlayer.Add(r);
                        }
                        else
                            if (this.regionDictionary[r.Start].GetSettlementByOwner(playerID) != null)
                           // if regionDictionary contains region with position which means this position is already visited 
                            {
                                this.AddToBalancedList(reg, r, ref balancedList); // try to add and adjust 
                            }
                    }
                    regions.Remove(reg); // remove visited region, benefit to disconnected graph
                }
            }

            foreach (RegionInfo reg in regionsWhichAreNotBelongToPlayer)
            {
                balancedList.Remove(reg);
            }

            return balancedList;
        }

        /// <summary>
        /// Try to add position to adjancy list of key.
        /// At the same time try to satisfy:
        ///       if there are other key have this position 
        ///       try to find whether this key is nearest of all.
        ///       If so add position to this key,
        ///       else not add.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="position"></param>
        private void AddToBalancedList(
            RegionInfo key, 
            RegionInfo position,
            ref Dictionary<RegionInfo,
            HashSet<RegionInfo>> list)
        {
            Position keyPositionDiff = key.Start - position.Start;

            foreach (RegionInfo p in this.map.Values(key))
            {
                if (list.ContainsKey(p) == true) // if false which means this position is not visited, we don't need to conside it.
                    if (list[p].Contains(position) == true) // if not within p's list which means it is not a neighbor region
                    {
                        if ((p.Start - position.Start).GetAbsSum() > keyPositionDiff.GetAbsSum()) // find the nearest neighbor
                        {
                            this.ChangeBalancedList(p, key, position,ref list);
                        }
                    }
            }
        }

        /// <summary>
        /// Remove pos from remove's list. Add pos to add's list.
        /// </summary>
        /// <param name="pos1"></param>
        /// <param name="pos2"></param>
        /// <param name="pos"></param>
        private void ChangeBalancedList(
            RegionInfo remove, 
            RegionInfo add, 
            RegionInfo pos,
            ref Dictionary<RegionInfo, HashSet<RegionInfo>> list)
        {
            list[remove].Remove(pos);
            list[add].Add(pos);
        }

        #endregion

        #endregion

        #region Contains

        public bool ContainsRegion(Position pos)
        {
            return this.regionDictionary.ContainsKey(pos);
        }

        #endregion

    }
}
